import fs from 'node:fs'
import { fileURLToPath } from 'node:url'
import path from 'node:path'
import gulp from 'gulp'
import { deleteAsync } from 'del'
import log from 'fancy-log'
import webpack from 'webpack'
import babel from 'gulp-babel'
import { mkdirp } from 'mkdirp'
import { cleanup, iteratePath }  from './tools/docgenerator.js'
import { generateEntryFiles } from './tools/entryGenerator.js'
import { getAllFiles, validateChars } from './tools/validateAsciiChars.js'

const __dirname = path.dirname(fileURLToPath(import.meta.url))

const SRC_DIR = path.join(__dirname, '/src')
const BUNDLE_ENTRY = `${SRC_DIR}/defaultInstance.js`
const HEADER = `${SRC_DIR}/header.js`
const VERSION = `${SRC_DIR}/version.js`
const COMPILE_SRC = `${SRC_DIR}/**/*.?(c)js`
const COMPILE_ENTRY_SRC = `${SRC_DIR}/entry/**/*.js`

const COMPILE_DIR = path.join(__dirname, '/lib')
const COMPILE_BROWSER = `${COMPILE_DIR}/browser`
const COMPILE_CJS = `${COMPILE_DIR}/cjs`
const COMPILE_ESM = `${COMPILE_DIR}/esm` // es modules
const COMPILE_ENTRY_LIB = `${COMPILE_CJS}/entry`

const FILE = 'math.js'

const REF_SRC = SRC_DIR + '/'
const REF_DIR = path.join(__dirname, '/docs')
const REF_DEST = `${REF_DIR}/reference/functions`
const REF_ROOT = `${REF_DIR}/reference`

const MATH_JS = `${COMPILE_BROWSER}/${FILE}`
const COMPILED_HEADER = `${COMPILE_CJS}/header.js`

const PACKAGE_JSON_COMMONJS = '{\n  "type": "commonjs"\n}\n'

const AUTOGENERATED_WARNING = `
// Note: This file is automatically generated when building math.js.
// Changes made in this file will be overwritten.
`

// read the version number from package.json
function getVersion () {
  return JSON.parse(String(fs.readFileSync('./package.json'))).version
}

// generate banner with today's date and correct version
function createBanner () {
  const today = new Date().toISOString().substr(0, 10) // today, formatted as yyyy-mm-dd
  const version = getVersion()

  return String(fs.readFileSync(HEADER))
    .replace('@@date', today)
    .replace('@@version', version)
}

// generate a js file containing the version number
function updateVersionFile (done) {
  const version = getVersion()

  fs.writeFileSync(VERSION, `export const version = '${version}'${AUTOGENERATED_WARNING}`)

  done()
}

const bannerPlugin = new webpack.BannerPlugin({
  banner: createBanner(),
  entryOnly: true,
  raw: true
})

const babelConfig = JSON.parse(String(fs.readFileSync('./.babelrc')))

const webpackConfig = {
  entry: BUNDLE_ENTRY,
  mode: 'production',
  performance: { hints: false }, // to hide the "asset size limit" warning
  output: {
    library: 'math',
    libraryTarget: 'umd',
    libraryExport: 'default',
    path: COMPILE_BROWSER,
    globalObject: 'this',
    filename: FILE
  },
  node: false, // to make sure Webpack doesn't generate 'new Function("return this")' in the bundle output, see https://github.com/josdejong/mathjs/issues/3001
  plugins: [
    bannerPlugin
    // new webpack.optimize.ModuleConcatenationPlugin()
    // TODO: ModuleConcatenationPlugin seems not to work. https://medium.com/webpack/webpack-3-official-release-15fd2dd8f07b
  ],
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: babelConfig
        }
      }
    ]
  },
  devtool: 'source-map',
  cache: true
}

// create a single instance of the compiler to allow caching
const compiler = webpack(webpackConfig)

function bundle (done) {
  // update the banner contents (has a date in it which should stay up to date)
  bannerPlugin.banner = createBanner()

  compiler.run(function (err, stats) {
    if (err) {
      log(err)
      done(err)
    }
    const info = stats.toJson()

    if (stats.hasWarnings()) {
      log('Webpack warnings:\n' + info.warnings.join('\n'))
    }

    if (stats.hasErrors()) {
      log('Webpack errors:\n' + info.errors.join('\n'))
      done(new Error('Compile failed'))
    }

    // create commonjs package.json file
    fs.writeFileSync(path.join(COMPILE_BROWSER, 'package.json'), PACKAGE_JSON_COMMONJS)

    log(`bundled ${MATH_JS}`)

    done()
  })
}

function compileCommonJs () {
  // create a package.json file in the commonjs folder
  mkdirp.sync(COMPILE_CJS)
  fs.writeFileSync(path.join(COMPILE_CJS, 'package.json'), PACKAGE_JSON_COMMONJS)

  return gulp.src(COMPILE_SRC)
    .pipe(babel())
    .pipe(gulp.dest(COMPILE_CJS))
}

function compileESModules () {
  return gulp.src(COMPILE_SRC)
    .pipe(babel({
      ...babelConfig,
      presets: [
        ['@babel/preset-env', {
          modules: false,
          targets: {
            esmodules: true
          }
        }]
      ]
    }))
    .pipe(gulp.dest(COMPILE_ESM))
}

function compileEntryFiles () {
  return gulp.src(COMPILE_ENTRY_SRC)
    .pipe(babel())
    .pipe(gulp.dest(COMPILE_ENTRY_LIB))
}

function writeCompiledHeader (cb) {
  fs.writeFileSync(COMPILED_HEADER, createBanner())
  cb()
}

function validateAscii (done) {
  const Reset = '\x1b[0m'
  const BgRed = '\x1b[41m'

  getAllFiles(SRC_DIR)
    .map(validateChars)
    .forEach(function (invalidChars) {
      invalidChars.forEach(function (res) {
        console.log(res.insideComment ? '' : BgRed,
          'file:', res.filename,
          'ln:' + res.ln,
          'col:' + res.col,
          'inside comment:', res.insideComment,
          'code:', res.c,
          'character:', String.fromCharCode(res.c),
          Reset
        )
      })
    })

  done()
}

async function generateDocs (done) {
  const all = (await import('file://' + REF_SRC + 'defaultInstance.js')).default
  const functionNames = Object.keys(all)
    .filter(key => typeof all[key] === 'function')

  if (functionNames.length === 0) {
    throw new Error('No function names found, is the doc generator broken?')
  }

  cleanup(REF_DEST, REF_ROOT)
  iteratePath(functionNames, REF_SRC, REF_DEST, REF_ROOT)

  done()
}

function generateEntryFilesCallback (done) {
  generateEntryFiles().then(() => {
    done()
  })
}

/**
 * Remove generated files
 *
 * @returns {Promise<string[]> | *}
 */
async function clean () {
  await deleteAsync([
    // legacy compiled files
    './es/',

    // generated browser bundle, esm code, and commonjs code
    './lib/',

    // generated source files
    'src/**/*.generated.js'
  ])
}

gulp.task('browser', bundle)

gulp.task('clean', clean)

gulp.task('docs', generateDocs)

// check whether any of the source files contains non-ascii characters
gulp.task('validate:ascii', validateAscii)

// The watch task (to automatically rebuild when the source code changes)
gulp.task('watch', function watch () {
  const files = ['package.json', 'src/**/*.js']
  const options = {
    // ignore version.js else we get an infinite loop since it's updated during bundle
    ignored: /version\.js/,
    ignoreInitial: false,
    delay: 100
  }

  gulp.watch(files, options, gulp.parallel(bundle, compileCommonJs))
})

// The default task (called when you run `gulp`)
gulp.task('default', gulp.series(
  clean,
  updateVersionFile,
  generateEntryFilesCallback,
  compileCommonJs,
  compileEntryFiles,
  compileESModules, // Must be after generateEntryFilesCallback
  writeCompiledHeader,
  bundle,
  generateDocs
))
